﻿using System;
using System.Collections.Generic;
using System.IO;
using OpenXml.Internal;
using OpenXml.Internal.IO.Packaging;
using OpenXml.Internal.IO.Zip;
using OpenXml.IO.CTProviders;
using OpenXml.Utility;

namespace OpenXml.IO.Packaging
{
    public class Package : IDisposable
    {
        private static readonly string FORWARD_SLASH = "/";

        private static readonly FileAccess DEFAULT_FILE_ACCESS = FileAccess.ReadWrite;
        private static readonly FileMode DEFAULT_FILE_MODE = FileMode.OpenOrCreate;
        private static readonly FileShare DEFAULT_FILE_SHARE = FileShare.None;
        private static readonly FileAccess DEFAULT_STREAM_ACCESS = FileAccess.Read;
        private static readonly FileMode DEFAULT_STREAM_MODE = FileMode.Open;
        private static readonly Boolean DEFAULT_STREAMING = false;
        
        #region Properties

        private bool _inStreamingCreation;
        public bool InStreamingCreation
        {
            get
            {
                return this._inStreamingCreation;
            }
        }

        public Boolean Disposed { get; set; }

        private FileAccess _openFileAccess;
        public FileAccess FileOpenAccess
        {
            get
            {
                this.ThrowIfObjectDisposed();
                return this._openFileAccess;
            }
        }

        public PartBasedPackageProperties PackageProperties { get; set; }

        protected Stream ContainerStream { get; set; }

        public ZipArchive Archive { get; set; }

        private InternalRelationshipCollection Relationships { get; set; }

        private SortedList<PackUriHelper.ValidatedPartUri, PackagePart> _partList;

        private ContentTypeProvider _contentTypeProvider;

        #endregion

        public Package(String path, FileMode packageMode, FileAccess packageAccess, FileShare packageShare, ContentTypeProvider ctProvider)
            : this(path, packageMode, packageAccess, packageShare, DEFAULT_STREAMING, ctProvider) { }

        public Package(String path, FileMode packageMode, FileAccess packageAccess, ContentTypeProvider ctProvider)
            : this(path, packageMode, packageAccess, DEFAULT_FILE_SHARE, DEFAULT_STREAMING, ctProvider) { }

        public Package(String path, FileMode packageMode, ContentTypeProvider ctProvider)
            : this(path, packageMode, DEFAULT_FILE_ACCESS, DEFAULT_FILE_SHARE, DEFAULT_STREAMING, ctProvider) { }

        public Package(String path, ContentTypeProvider ctProvider)
            : this(path, DEFAULT_FILE_MODE, DEFAULT_FILE_ACCESS, DEFAULT_FILE_SHARE, DEFAULT_STREAMING, ctProvider) { }

        public Package(String path, FileMode packageMode, FileAccess packageAccess, FileShare packageShare, bool streaming, ContentTypeProvider ctProvider)
        {
            if (path == null) { throw new ArgumentNullException("path"); }
            this.Archive = ZipArchive.OpenOnFile(path, packageMode, packageAccess, packageShare, streaming);
            Initialize(null, packageAccess, streaming, ctProvider);
        }

        public Package(Stream stream, FileMode packageMode, FileAccess packageAccess, ContentTypeProvider ctProvider) 
            : this(stream, packageMode, packageAccess, DEFAULT_STREAMING, ctProvider) { }

        public Package(Stream stream, FileMode packageMode, ContentTypeProvider ctProvider)
            : this(stream, packageMode, DEFAULT_STREAM_ACCESS, DEFAULT_STREAMING, ctProvider) { }

        public Package(Stream stream, ContentTypeProvider ctProvider)
            : this(stream, DEFAULT_STREAM_MODE, DEFAULT_STREAM_ACCESS, DEFAULT_STREAMING, ctProvider) { }

        public Package(Stream stream, FileMode packageMode, FileAccess packageAccess, bool streaming, ContentTypeProvider ctProvider)
        {
            if (stream == null) { throw new ArgumentNullException("stream"); }
            ThrowIfFileAccessInvalid(packageAccess);

            this.Archive = ZipArchive.OpenOnStream(stream, packageMode, packageAccess, streaming);
            Initialize(stream, packageAccess, streaming, ctProvider);
        }

        private void Initialize(Stream stream, FileAccess packageAccess, bool streaming, ContentTypeProvider ctProvider)
        {
            this._openFileAccess = packageAccess;
            this._inStreamingCreation = (packageAccess == FileAccess.Write) && streaming;
            this._partList = new SortedList<PackUriHelper.ValidatedPartUri, PackagePart>();

            this.ContainerStream = stream;
            this.Disposed = false;

            this._contentTypeProvider = ctProvider.Create(this.Archive);

            if (!this._inStreamingCreation && ((this.FileOpenAccess == FileAccess.ReadWrite) || (this.FileOpenAccess == FileAccess.Read)))
            {
                GetParts();
            }
        }

        public void Close()
        {
            ((IDisposable)this).Dispose();
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!this.Disposed && disposing)
            {
                this._partList.Clear();
                if (this.PackageProperties != null)
                {
                    this.PackageProperties.Dispose();
                    this.PackageProperties = null;
                }
                this._partList = null;
                this.Relationships = null;
                this.Disposed = true;
            }
        }

        void IDisposable.Dispose()
        {
            if (!this.Disposed)
            {
                try
                {
                    if (this.PackageProperties != null)
                    {
                        this.PackageProperties.Close();
                    }
                    if (this.InStreamingCreation)
                    {
                        this.ClosePackageRelationships();
                    }
                    else
                    {
                        this.FlushRelationships();
                    }
                    this.DoOperationOnEachPart(DoCloseRelationshipsXml);
                    this.DoOperationOnEachPart(this.DoClose);
                    this.Dispose(true);
                }
                finally
                {
                    this.Disposed = true;
                }
                GC.SuppressFinalize(this);
            }
        }

        private static bool DoCloseRelationshipsXml(PackagePart part)
        {
            if (!part.IsRelationshipPart)
            {
                part.CloseRelationships();
            }
            return true;
        }

        private bool DoClose(PackagePart part)
        {
            if (!part.IsClosed)
            {
                if (PackUriHelper.IsRelationshipPartUri(part.Uri) && (PackUriHelper.ComparePartUri(part.Uri, PackageRelationship.ContainerRelationshipPartName) != 0))
                {
                    PackagePart packagePart;
                    PackUriHelper.ValidatedPartUri sourcePartUriFromRelationshipPartUri = (PackUriHelper.ValidatedPartUri)PackUriHelper.GetSourcePartUriFromRelationshipPartUri(part.Uri);
                    if (this._partList.TryGetValue(sourcePartUriFromRelationshipPartUri, out packagePart))
                    {
                        packagePart.Close();
                    }
                }
                part.Close();
            }
            return true;
        }

        private void ThrowIfObjectDisposed()
        {
            if (this.Disposed)
            {
                throw new ObjectDisposedException(null, SR.Get("ObjectDisposed"));
            }
        }

        internal static void ThrowIfFileAccessInvalid(FileAccess access)
        {
            if ((access < FileAccess.Read) || (access > FileAccess.ReadWrite))
            {
                throw new ArgumentOutOfRangeException("access");
            }
        }

        internal void ThrowIfReadOnly()
        {
            if (this._openFileAccess == FileAccess.Read)
            {
                throw new IOException(SR.Get("CannotModifyReadOnlyContainer"));
            }
        }

        internal void ThrowIfWriteOnly()
        {
            if (this._openFileAccess == FileAccess.Write)
            {
                throw new IOException(SR.Get("CannotRetrievePartsOfWriteOnlyContainer"));
            }
        }

        internal void ClosePackageRelationships()
        {
            this.ThrowIfNotInStreamingCreation("ClosePackageRelationships");
            if (this.Relationships != null)
            {
                this.Relationships.CloseInStreamingCreationMode();
            }
        }

        internal void ThrowIfNotInStreamingCreation(string methodName)
        {
            if (!this.InStreamingCreation)
            {
                throw new IOException(SR.Get("MethodAvailableOnlyInStreamingCreation", new object[] { methodName }));
            }
        }

        private void FlushRelationships()
        {
            if ((this.Relationships != null) && (this._openFileAccess != FileAccess.Read))
            {
                this.Relationships.Flush();
            }
        }

        internal delegate bool PartOperation(PackagePart part);

        private void DoOperationOnEachPart(PartOperation operation)
        {
            if (this._partList.Count > 0)
            {
                int num = 0;

                PackUriHelper.ValidatedPartUri[] uriArray = new PackUriHelper.ValidatedPartUri[this._partList.Keys.Count];
                foreach (PackUriHelper.ValidatedPartUri uri in this._partList.Keys)
                {
                    uriArray[num++] = uri;
                }
                for (int i = 0; i < this._partList.Keys.Count; i++)
                {
                    PackagePart part;
                    if (this._partList.TryGetValue(uriArray[i], out part) && !operation(part))
                    {
                        return;
                    }
                }
            }
        }

        public PackagePart GetPart(Uri partUri)
        {
            PackagePart partHelper = this.GetPartHelper(partUri);
            if (partHelper == null)
            {
                throw new InvalidOperationException(SR.Get("PartDoesNotExist"));
            }
            return partHelper;
        }

        //TODO: Реализация только в наследниках
        protected virtual PackagePart GetPartCore(Uri partUri)
        {
            return null;
        }

        private PackagePart GetPartHelper(Uri partUri)
        {
            this.ThrowIfObjectDisposed();
            this.ThrowIfWriteOnly();
            if (partUri == null)
            {
                throw new ArgumentNullException("partUri");
            }
            PackUriHelper.ValidatedPartUri key = PackUriHelper.ValidatePartUri(partUri);
            if (this._partList.ContainsKey(key))
            {
                return this._partList[key];
            }
            PackagePart partCore = this.GetPartCore(key);
            if (partCore != null)
            {
                this.AddIfNoPrefixCollisionDetected(key, partCore);
            }
            return partCore;
        }

        public PackagePart CreatePart(Uri partUri, string contentType)
        {
            return this.CreatePart(partUri, contentType, CompressionOption.NotCompressed);
        }

        public PackagePart CreatePart(Uri partUri, string contentType, CompressionOption compressionOption)
        {
            this.ThrowIfObjectDisposed();
            this.ThrowIfReadOnly();
            if (partUri == null)
            {
                throw new ArgumentNullException("partUri");
            }
            if (contentType == null)
            {
                throw new ArgumentNullException("contentType");
            }
            ThrowIfCompressionOptionInvalid(compressionOption);
            PackUriHelper.ValidatedPartUri key = PackUriHelper.ValidatePartUri(partUri);
            if (this._partList.ContainsKey(key))
            {
                throw new InvalidOperationException(SR.Get("PartAlreadyExists"));
            }
            this.AddIfNoPrefixCollisionDetected(key, null);
            PackagePart part = this.CreatePartCore(key, contentType, compressionOption);
            this._partList[key] = part;
            return part;
        }

        //TODO: Реализация только в наследниках
        protected virtual PackagePart CreatePartCore(Uri partUri, string contentType, CompressionOption compressionOption)
        {
            return null;
        }

        public PackageRelationship CreateRelationship(Uri targetUri, TargetMode targetMode, string relationshipType)
        {
            return this.CreateRelationship(targetUri, targetMode, relationshipType, null);
        }

        public PackageRelationship CreateRelationship(Uri targetUri, TargetMode targetMode, string relationshipType, string id)
        {
            this.ThrowIfObjectDisposed();
            this.ThrowIfReadOnly();
            this.EnsureRelationships();
            return this.Relationships.Add(targetUri, targetMode, relationshipType, id);
        }

        public void DeletePart(Uri partUri)
        {
            this.ThrowIfObjectDisposed();
            this.ThrowIfReadOnly();
            this.ThrowIfInStreamingCreation("DeletePart");
            if (partUri == null)
            {
                throw new ArgumentNullException("partUri");
            }
            PackUriHelper.ValidatedPartUri key = PackUriHelper.ValidatePartUri(partUri);
            if (this._partList.ContainsKey(key))
            {
                key = (PackUriHelper.ValidatedPartUri)this._partList[key].Uri;
                this._partList[key].IsDeleted = true;
                this._partList[key].Close();
                this.DeletePartCore(key);
                this._partList.Remove(key);
            }
            else
            {
                this.DeletePartCore(key);
            }
            if (PackUriHelper.IsRelationshipPartUri(key))
            {
                Uri sourcePartUriFromRelationshipPartUri = PackUriHelper.GetSourcePartUriFromRelationshipPartUri(key);
                if (Uri.Compare(sourcePartUriFromRelationshipPartUri, PackUriHelper.PackageRootUri, UriComponents.SerializationInfoString, UriFormat.UriEscaped, StringComparison.Ordinal) == 0)
                {
                    this.ClearRelationships();
                }
                else if (this.PartExists(sourcePartUriFromRelationshipPartUri))
                {
                    this.GetPart(sourcePartUriFromRelationshipPartUri).ClearRelationships();
                }
            }
            else
            {
                this.DeletePart(PackUriHelper.GetRelationshipPartUri(key));
            }
        }

        //TODO: Реализация только в наследниках
        protected virtual void DeletePartCore(Uri partUri)
        {
        }

        public void DeleteRelationship(string id)
        {
            this.ThrowIfObjectDisposed();
            this.ThrowIfReadOnly();
            this.ThrowIfInStreamingCreation("DeleteRelationship");
            if (id == null)
            {
                throw new ArgumentNullException("id");
            }
            InternalRelationshipCollection.ThrowIfInvalidXsdId(id);
            this.EnsureRelationships();
            this.Relationships.Delete(id);
        }

        private void EnsureRelationships()
        {
            if (this.Relationships == null)
            {
                this.Relationships = new InternalRelationshipCollection(this);
            }
        }

        private void ClearRelationships()
        {
            if (this.Relationships != null)
            {
                this.Relationships.Clear();
            }
        }

        private void AddIfNoPrefixCollisionDetected(PackUriHelper.ValidatedPartUri partUri, PackagePart part)
        {
            this._partList.Add(partUri, part);
            int num = this._partList.IndexOfKey(partUri);
            Invariant.Assert(num >= 0, "Given uri must be present in the dictionary");
            string normalizedPartUriString = partUri.NormalizedPartUriString;
            string str2 = null;
            string str3 = null;
            if (num > 0)
            {
                str2 = this._partList.Keys[num - 1].NormalizedPartUriString;
            }
            if (num < (this._partList.Count - 1))
            {
                str3 = this._partList.Keys[num + 1].NormalizedPartUriString;
            }
            if ((((str2 != null) && normalizedPartUriString.StartsWith(str2, StringComparison.Ordinal)) && ((normalizedPartUriString.Length > str2.Length) && (normalizedPartUriString[str2.Length] == PackUriHelper.ForwardSlashChar))) || (((str3 != null) && str3.StartsWith(normalizedPartUriString, StringComparison.Ordinal)) && ((str3.Length > normalizedPartUriString.Length) && (str3[normalizedPartUriString.Length] == PackUriHelper.ForwardSlashChar))))
            {
                this._partList.Remove(partUri);
                throw new InvalidOperationException(SR.Get("PartNamePrefixExists"));
            }
        }

        public virtual bool PartExists(Uri partUri)
        {
            return (this.GetPartHelper(partUri) != null);
        }

        internal static void ThrowIfCompressionOptionInvalid(CompressionOption compressionOption)
        {
            if ((compressionOption < CompressionOption.NotCompressed) || (compressionOption > CompressionOption.SuperFast))
            {
                throw new ArgumentOutOfRangeException("compressionOption");
            }
        }

        internal static void ThrowIfFileModeInvalid(FileMode mode)
        {
            if ((mode < FileMode.CreateNew) || (mode > FileMode.Append))
            {
                throw new ArgumentOutOfRangeException("mode");
            }
        }

        internal void ThrowIfInStreamingCreation(string methodName)
        {
            if (this._inStreamingCreation)
            {
                throw new IOException(SR.Get("OperationIsNotSupportedInStreamingProduction", new object[] { methodName }));
            }
        }

        internal static void GetZipCompressionMethodFromOpcCompressionOption(CompressionOption compressionOption, out CompressionMethodEnum compressionMethod, out DeflateOptionEnum deflateOption)
        {
            switch (compressionOption)
            {
                case CompressionOption.Normal:
                    compressionMethod = CompressionMethodEnum.Deflated;
                    deflateOption = DeflateOptionEnum.Normal;
                    return;

                case CompressionOption.Maximum:
                    compressionMethod = CompressionMethodEnum.Deflated;
                    deflateOption = DeflateOptionEnum.Maximum;
                    return;

                case CompressionOption.Fast:
                    compressionMethod = CompressionMethodEnum.Deflated;
                    deflateOption = DeflateOptionEnum.Fast;
                    return;

                case CompressionOption.SuperFast:
                    compressionMethod = CompressionMethodEnum.Deflated;
                    deflateOption = DeflateOptionEnum.SuperFast;
                    return;
            }
            compressionMethod = CompressionMethodEnum.Stored;
            deflateOption = DeflateOptionEnum.None;
        }

        public void GetParts()
        {
            this.ThrowIfObjectDisposed();
            this.ThrowIfWriteOnly();
            PackagePart[] partsCore = this.GetPartsCore();
            Dictionary<PackUriHelper.ValidatedPartUri, PackagePart> dictionary = new Dictionary<PackUriHelper.ValidatedPartUri, PackagePart>(partsCore.Length);
            foreach (PackagePart part in partsCore)
            {
                PackUriHelper.ValidatedPartUri key = (PackUriHelper.ValidatedPartUri)part.Uri;
                if (dictionary.ContainsKey(key))
                {
                    throw new FileFormatException(SR.Get("BadPackageFormat"));
                }
                dictionary.Add(key, part);
                if (!this._partList.ContainsKey(key))
                {
                    this.AddIfNoPrefixCollisionDetected(key, part);
                }
            }
        }

        protected PackagePart[] GetPartsCore()
        {
            List<PackagePart> parts = new List<PackagePart>(50);
            SortedDictionary<PieceInfo, object> pieceDictionary = new SortedDictionary<PieceInfo, object>(PieceNameHelper.PieceNameComparer);
            foreach (ZipFileInfo info in this.Archive.GetFiles())
            {
                PackUriHelper.ValidatedPartUri uri4;
                if (IsValidFileItem(info))
                {
                    if (this.IsZipItemValidOpcPartOrPiece(info.Name))
                    {
                        PieceInfo info2;
                        if (PieceNameHelper.TryCreatePieceInfo(info, out info2))
                        {
                            if (pieceDictionary.ContainsKey(info2))
                            {
                                throw new FormatException(SR.Get("DuplicatePiecesFound"));
                            }
                            if (info2.PartUri != null)
                            {
                                pieceDictionary.Add(info2, null);
                            }
                        }
                        else
                        {
                            PackUriHelper.ValidatedPartUri uri2;
                            Uri uri = new Uri(GetOpcNameFromZipItemName(info.Name), UriKind.Relative);
                            if (PackUriHelper.TryValidatePartUri(uri, out uri2))
                            {
                                ContentType contentType = this._contentTypeProvider.GetContentType(uri2);
                                if (contentType != null)
                                {
                                    parts.Add(new ZipPackagePart(this, info, uri2, contentType.ToString(), GetCompressionOptionFromZipFileInfo(info)));
                                }
                            }
                        }
                    }
                    continue;
                }
            }
            return parts.ToArray();
        }

        private static bool IsValidFileItem(ZipFileInfo info)
        {
            return (!info.FolderFlag && !info.VolumeLabelFlag);
        }

        private bool IsZipItemValidOpcPartOrPiece(string zipItemName)
        {
            if (zipItemName.StartsWith(_contentTypeProvider.ContentTypeFileName, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
            if (zipItemName.StartsWith(FORWARD_SLASH, StringComparison.Ordinal))
            {
                return false;
            }
            if (zipItemName.EndsWith(FORWARD_SLASH, StringComparison.Ordinal))
            {
                return false;
            }
            return true;
        }


        internal static string GetOpcNameFromZipItemName(string zipItemName)
        {
            return (FORWARD_SLASH + zipItemName);
        }

        private static CompressionOption GetCompressionOptionFromZipFileInfo(ZipFileInfo zipFileInfo)
        {
            CompressionOption notCompressed = CompressionOption.NotCompressed;
            if (zipFileInfo.CompressionMethod == CompressionMethodEnum.Deflated)
            {
                switch (zipFileInfo.DeflateOption)
                {
                    case DeflateOptionEnum.Normal:
                        return CompressionOption.Normal;

                    case ((DeflateOptionEnum)1):
                    case ((DeflateOptionEnum)3):
                    case ((DeflateOptionEnum)5):
                        return notCompressed;

                    case DeflateOptionEnum.Maximum:
                        return CompressionOption.Maximum;

                    case DeflateOptionEnum.Fast:
                        return CompressionOption.Fast;

                    case DeflateOptionEnum.SuperFast:
                        return CompressionOption.SuperFast;

                    case DeflateOptionEnum.None:
                        return notCompressed;
                }
            }
            return notCompressed;
        }
    }
}
